from . import models
import simplejson
from google.appengine.ext import webapp
from google.appengine.api import users
import calendar
import cgi
import datetime
import functools
import logging
import re
import time

# set_status
# out.truncate(0)
# out.write

def user_objects():
    """Return a query containing all of the objects for the current
    user."""
    user = users.get_current_user()
    return models.DataObject.all().filter('user =', user)


def dt_to_epoch(dt):
    return calendar.timegm(dt.utctimetuple())


class RewriteHistory(webapp.RequestHandler):

    """Receive changes from the browser w/o sending anything back."""

    def post(self):
        data = simplejson.loads(self.request.body)
        new_timestamp = datetime.datetime.now()
        conflicts = {}
        for guid, timestamp, action, json in data:
            logging.info("got pushed %s", guid)
            ob = user_objects().filter('guid =', guid).get()
            dt = datetime.datetime.utcfromtimestamp(timestamp)
            if action == 'c':
                if ob is not None:
                    logging.error('push tried to create a GUID that existed: %s',
                                    guid)
                    conflicts[guid] = json
                    continue
                ob = models.DataObject(guid=guid, json=json, updated=new_timestamp,
                                        session='N/A')
            elif action == 'u':
                if ob is None:
                    logging.error('tried to update non-existent %s', guid)
                    conflicts[guid] = json
                    continue
                if dt < ob.updated:
                    logging.error('tried to update %s w/ stale data', guid)
                    conflicts[guid] = json
                    continue
                ob.json = json
                ob.updated = dt
            elif action == 'd':
                if dt < ob.updated:
                    logging.error('tried to del %s before seeing changes',
                                    guid)
                    conflicts[guid] = None
                    continue
                ob.json = None
                ob.updated = dt
            else:
                logging.error('you cannot push the action of %s for %s',
                                action, guid)
            ob.put()
        json = [dt_to_epoch(newtimestamp), conflicts]
        self.response.out.write(json)


class BitOfSugar(webapp.RequestHandler):

    """Return all objects from a specific timestamp."""

    def post(self):
        raw_timestamp = simplejson.loads(self.request.body)
        browser_timestamp = datetime.datetime.utcfromtimestamp(raw_timestamp)
        obs = user_objects().filter('updated > ', browser_timestamp)
        obs.order('updated')
        send_back = {}
        newest_timestamp = None
        epoch = -1
        for ob in obs:
            if newest_timestamp is None:
                newest_timestamp = ob.updated
            else:
                if ob.updated != newest_timestamp:
                    logging.info("more to send: %r != %r",
                                    ob.updated, newest_timestamp)
                    break
            if ob.json is not None:
                send_back[ob.guid] = ob.json
        else:
            logging.info("all done")
            newest_timestamp = -1
        if newest_timestamp == -1:
            epoch = -1
        else:
            epoch = dt_to_epoch(newest_timestamp)
        logging.info("Pulled until %s, returning %r", epoch, send_back)
        json = simplejson.dumps([epoch, send_back])
        self.response.out.write(json)


class Gimme(webapp.RequestHandler):

    """Return a specific object and friends in the same group."""

    def post(self):
        id_ = self.request.body
        user_obs = user_objects()
        requested = user_obs.filter('guid =', id_).get()
        if not requested:
            json = ''
            logging.error("%s does not exist", id_)
        else:
            timestamp = requested.updated
            epoch = dt_to_epoch(timestamp)
            group = requested.group
            logging.info("using group %s", group)
            others = user_objects().filter('group =', group)
            friends = {}
            for ob in others:
                friends[ob.guid] = ob.json
            json = simplejson.dumps([epoch, group, friends])
            logging.info("found %s and %s friends: %r",
                            id_, len(friends) - 1, friends)
        self.response.out.write(json)


class Groupies(webapp.RequestHandler):

    """Return a group of objects."""

    def post(self):
        group = simplejson.loads(self.request.body)
        logging.info("Asked for group %r", group)
        user_obs = user_objects()
        group_to_use = getattr(user_obs.filter('group >=', group).order('group').get(), 'group', None)
        logging.info("Found group %r", group_to_use)
        group_obs = user_objects().filter('group =', group_to_use)
        obs = {}
        for ob in group_obs:
            obs[ob.guid] = ob.json
        logging.info("Sending %r", obs)
        self.response.out.write(simplejson.dumps([group_to_use, obs]))


class Clear(webapp.RequestHandler):

    """Clear out the datastore for the user."""

    # XXX Must be logged in.
    def get(self):
        """() -> None"""
        # XXX Done in a transaction
        for ob in user_objects():
            ob.delete()


class OnTheList(webapp.RequestHandler):

    """Verify the user is logged in, otherwise specify where the user needs to
    go to log in."""

    def post(self):
        """(redirect:str) -> str

        If the user is logged in, return nothing. Else return the URL where the
        user can log in.

        """
        self.response.headers['Content-Type'] = 'text/plain'
        user = users.get_current_user()
        if user is not None:
            return
        redirect_url = self.request.body
        if not redirect_url:
            self.response.set_status(400)
            return
        # XXX Send 40X that the user is not logged in?
        self.response.out.write(users.create_login_url(redirect_url))


class WhatsNew(webapp.RequestHandler):

    """Synchronize with a browser."""

    # XXX Must be logged in
    def post(self):
        """(log:dict, timestamp:float, session:str) -> [float, dict, list]

        Update the server based on what the browser has mutated. The log dict
        is keyed on GUID and contains values of lists specifying the
        action of the update and optionally the new value for the object.

        A list with three items is returned. The first item contains the
        timestamp from the server based on what this sync represents.
        The second item is a dict keyed on GUID containing the JSON
        representing the new values of object mutated since the last sync.
        The third item is a list of GUIDs that were sent from the browser that
        conflict with what the server has. The dict being sent back will
        include the new values of these objects.

        Any objects with a JSON value of null is considered deleted.

        """
        try:
            log, raw_timestamp, session = simplejson.loads(self.request.body)
            logging.info("Session %r; last sync @ %r", session, raw_timestamp)
        except ValueError, exc:
            self.response.set_status(400)
            return
        browser_timestamp = datetime.datetime.utcfromtimestamp(raw_timestamp)
        conflicts = []
        logging.info("%s items in the log from the browser", len(log))
        logging.info("%r", log)
        new_timestamp = time.time()
        new_stamp = datetime.datetime.utcfromtimestamp(new_timestamp)
        for guid, details in log.iteritems():
            action, json = details
            # TODO Error-check as this is a security issue.
            args = guid, json, new_stamp, session
            if not getattr(self, action+'_object')(*args):
                conflicts.append(guid)
        query = user_objects()
        new_objects = query.filter('updated >', browser_timestamp)
        changes = {}
        for dob in new_objects:
            if dob.session != session:
                changes[dob.guid] = dob.json
        results = [new_timestamp, changes, conflicts]
        logging.info("Sending back %s items, %s conflicts @ %s",
                len(changes), len(conflicts), new_timestamp)
        json_results = simplejson.dumps(results)
        self.response.headers['Content-Type'] = 'application/json'
        self.response.out.write(json_results)


    def object_by_guid(self, guid):
        return user_objects().filter('guid =', guid).get()

    def create_object(self, guid, json, timestamp, session):
        dob = self.object_by_guid(guid)
        if dob is not None:
            # XXX Error; can't create an object that already exists.
            logging.error("Can't create %r as it already exists", guid)
            return False
        ob = simplejson.loads(json)
        group = ob.get('__group__', None)
        dob = models.DataObject(guid=guid, json=json, updated=timestamp,
                                session=session, group=group)
        dob.put()
        return True

    def update_object(self, guid, json, timestamp, session):
        dob = self.object_by_guid(guid)
        if dob is None:
            # XXX Forces ID format to be known to server. Better way to handle
            #     when a new browser is first loaded and the root is not
            #     considered created?
            if guid.endswith('root'):
                # Implicit root
                return self.create_object(guid, json, timestamp, session)
            else:
                # XXX Error; can't update a non-existent object.
                logging.error("can't update %r as it doesn't exist", guid)
        elif dob.json is None:
            # XXX Error; can't update a deleted object.
            # XXX Is this the best solution?
            logging.error("can't update %r as it has been deleted", guid)
            return False
        elif timestamp < dob.updated:
            # XXX Error; can't update w/ a newer timestamp.
            logging.error("update for %r out of date; %s > %s",
                            guid, timestamp, dob.updated)
            return False
        dob.json = json
        dob.session = session
        dob.updated = timestamp
        dob.group = simplejson.loads(json).get('__group__', None)
        dob.put()
        return True

    def delete_object(self, guid, json, timestamp, session):
        dob = self.object_by_guid(guid)
        if dob is None:
            # XXX Error; can't delete a non-existent object
            logging.error("can't delete %r as it doesn't exist", guid)
        elif dob.json is None:
            # XXX Error; can't delete an object that is already deleted.
            logging.error("can't delete %r as it was already deleted", guid)
            return False
        # XXX Do in a transaction
        dob.session = session
        dob.json = None
        dob.updated = timestamp
        dob.put()
        return True
